Introducción al manejo de datos con R
Comentarios iniciales y objetivos
En este documento se pretende hacer un repaso rápido de algunas de las posibilidades disponibles para el manejo de conjuntos de datos en R, con el objetivo fundamental de saber cómo gestionar la información contenida en los datos para extraer solamente la parte que resulte de interés en cada aplicación.
Análisis inicial de un conjunto de datos
Como data scientists modelizadores nos llegarán datos de muy diferente índole, posiblemente en distintos formatos y algunas veces muy “sucios”. Nuestro trabajo es resumir que demonios hay en el conjunto de datos y qué información interesante podríamos utilizar para los objetivos concretos del proyecto de modelización.
En primer lugar, necesitamos tener una idea de tamaño y disposición del archivo. Cuantas variables y registros tenemos, sus tipos, si todo parece correcto en cuanto a codificación o se ven cosas raras, esto a un nivel general. En particular, cada data set es un mundo y nos suscitará distintas cuestiones que debemos saber (auto)resolver para no quedarnos con dudas y tener el mayor conocimiento sobre los datos antes de modelar " a lo loco".
En este ejemplo, nos llega un dataset en formato RDS (formato comprimido de R para guardar objetos de todo tipo). En primer lugar, saber como leerlo! La función adecuada en este caso es readRDS(‘ruta al archivo/archivoRDS.RDS’). Como nos han dicho que el proyecto trata de consumo de medios, llamaremos al archivo datosMedia.
Visualización de los datos en bruto
# Leemos los datos desde formato binario comprimido RDS (la mejor opción en tiempo y espacio para guardar cualquier tipo de objeto de R!)
datosMedia <- readRDS('F:/Documentos/Master Comercio 2021-22/Datos/datos_usoMedia.RDS')
# Salida típica de data.frame. Pinta todo hasta un cierto límite de filas suele ser peor para visualizar...por eso le pido que nos enseñe solamente head()
head(data.frame(datosMedia)) ## date IdPanelist Medio DiaSem DayParting Plat Grupo Week
## 1 2021-09-01 41240 TV_Antena3 miércoles Mañana TV A3Media 1
## 2 2021-09-01 42501 App_Facebook miércoles Tarde App RRSS 1
## 3 2021-09-01 73126 App_Instagram miércoles Mediodia App RRSS 1
## 4 2021-09-01 56200 App_Instagram miércoles Mediodia App RRSS 1
## 5 2021-09-01 22850 Web_youtube miércoles Tarde Web Youtube 1
## 6 2021-09-01 66366 App_Instagram miércoles Mediodia App RRSS 1
## hora Sex AgeGroup AdultsWithKids RCH
## 1 9 Female 25 - 34 FALSE TRUE
## 2 18 Male 45 + TRUE TRUE
## 3 16 Female 18 - 24 TRUE TRUE
## 4 14 Female 18 - 24 FALSE FALSE
## 5 19 Female 25 - 34 FALSE FALSE
## 6 15 Male 25 - 34 TRUE TRUE
# Salida típica de un tibble de dplyr (siempre corta la salid por filas y columnas y es cómodo para visualizar)
tibble(datosMedia)## # A tibble: 704,806 x 13
## date IdPanelist Medio DiaSem DayParting Plat Grupo Week hora Sex
## <date> <dbl> <chr> <ord> <fct> <chr> <chr> <int> <dbl> <fct>
## 1 2021-09-01 41240 TV_Ant~ miérc~ Mañana TV A3Me~ 1 9 Fema~
## 2 2021-09-01 42501 App_Fa~ miérc~ Tarde App RRSS 1 18 Male
## 3 2021-09-01 73126 App_In~ miérc~ Mediodia App RRSS 1 16 Fema~
## 4 2021-09-01 56200 App_In~ miérc~ Mediodia App RRSS 1 14 Fema~
## 5 2021-09-01 22850 Web_yo~ miérc~ Tarde Web Yout~ 1 19 Fema~
## 6 2021-09-01 66366 App_In~ miérc~ Mediodia App RRSS 1 15 Male
## 7 2021-09-01 86041 App_Tw~ miérc~ Tarde App RRSS 1 17 Male
## 8 2021-09-01 7112 TV_Ant~ miérc~ Madrugada TV A3Me~ 1 1 Male
## 9 2021-09-01 79769 App_Fa~ miérc~ Mañana App RRSS 1 13 Fema~
## 10 2021-09-01 85549 App_Yo~ miérc~ Mañana App Yout~ 1 13 Fema~
## # ... with 704,796 more rows, and 3 more variables: AgeGroup <fct>,
## # AdultsWithKids <fct>, RCH <fct>
# Salida típica de un data.table (enseña principio y final del archivo, lo cual es útil!)
data.table(datosMedia)## date IdPanelist Medio DiaSem DayParting Plat Grupo
## 1: 2021-09-01 41240 TV_Antena3 miércoles Mañana TV A3Media
## 2: 2021-09-01 42501 App_Facebook miércoles Tarde App RRSS
## 3: 2021-09-01 73126 App_Instagram miércoles Mediodia App RRSS
## 4: 2021-09-01 56200 App_Instagram miércoles Mediodia App RRSS
## 5: 2021-09-01 22850 Web_youtube miércoles Tarde Web Youtube
## ---
## 704802: 2021-09-30 77536 App_Instagram jueves Primetime App RRSS
## 704803: 2021-09-30 29602 App_Facebook jueves Tarde App RRSS
## 704804: 2021-09-30 4496 Web_twitter jueves Mediodia Web RRSS
## 704805: 2021-09-30 75216 TV_Telecinco jueves Primetime TV Mediaset
## 704806: 2021-09-30 75911 App_MiTele jueves Mediodia App Mediaset
## Week hora Sex AgeGroup AdultsWithKids RCH
## 1: 1 9 Female 25 - 34 FALSE TRUE
## 2: 1 18 Male 45 + TRUE TRUE
## 3: 1 16 Female 18 - 24 TRUE TRUE
## 4: 1 14 Female 18 - 24 FALSE FALSE
## 5: 1 19 Female 25 - 34 FALSE FALSE
## ---
## 704802: 5 23 Male 18 - 24 FALSE FALSE
## 704803: 5 16 Male 18 - 24 FALSE TRUE
## 704804: 5 14 Female 45 + TRUE TRUE
## 704805: 5 23 Female 25 - 34 FALSE TRUE
## 704806: 5 13 Male 18 - 24 FALSE FALSE
# Salida en formato tabal dinámica para nuestro documento html. Súper útil para visualización de tablas!
datatable(head(datosMedia,1000), options = list(autoWidth = TRUE, scrollX = TRUE),
caption = 'Datos Uso de Medios',
class = 'cell-border stripe nowrap')Tenemos ante nuestros ojos los datos de utilización de distintos medios de TV, Radio, Apps y Webs de cierto panel de individuos. Interesante, queremos saber cosas pero ya!
Conclusiones del primer vistazo
De momento deducimos:
El tamaño del archivo es de 704806 filas o registros y 13 columnas o variables. Archivo no excesivamente grande pero ya de tamaño considerable para según que cosas.
Las filas parecen identificar movimientos de usuarios de distintos medios y en diferentes momentos del tiempo, parece un conjunto dinámico…pero luego parece que tenemos información sobre los propios individuos o panelistas y eso es información estática. Con lo cual estamos ante un conjunto de carácter híbrido ya que contiene parte de evolución y parte de caracterización.
Para el análisis dinámico es relevante plantear cuestiones como la granularidad de los datos, que sería la unidad mínima de cambio de tiempo. Si prestamos atención a las columnas del archivo, descubrimos que tenemos la fecha (date) cuya unidad mínima de media es el día, entonces, ¿son datos diarios?. La respuesta es no! La fecha se mide en días pero no podemos asegurar que sean datos diarios ya que tenemos otras variables como dayParting (franja horaria) y hora!! (también tenemos semana y día de la semana pero con granularidad mayor o igual que la propia fecha así que nada…) Conclusión, podemos afirmar que tenemos datos de granularidad horaria! Bien!
Otro aspecto importante a tener en cuenta es el periodo de tiempo de los datos disponibles. En la salida de data.table podemos ver que la fecha de las últimas filas es 2021-09-30, si asumimos que los datos están ordenados por fecha, tenemos un mes de datos. Pero esto es arriesgado afirmarlo…no lo sabemos. Podemos pedir un summary() para ver el rango de variación de la variable fecha (y de todas las variables si queremos). Tenemos la opción del slice_max
## # A tibble: 23,900 x 13
## date IdPanelist Medio DiaSem DayParting Plat Grupo Week hora Sex
## <date> <dbl> <chr> <ord> <fct> <chr> <chr> <int> <dbl> <fct>
## 1 2021-09-30 19554 Web_tw~ jueves Primetime Web RRSS 5 21 Fema~
## 2 2021-09-30 58839 App_In~ jueves Mediodia App RRSS 5 16 Male
## 3 2021-09-30 1722 Web_tw~ jueves Mañana Web RRSS 5 9 Fema~
## 4 2021-09-30 57931 App_In~ jueves Mediodia App RRSS 5 15 Fema~
## 5 2021-09-30 82179 App_In~ jueves Mediodia App RRSS 5 16 Fema~
## 6 2021-09-30 53678 Web_yo~ jueves Mañana Web Yout~ 5 12 Fema~
## 7 2021-09-30 19581 App_Fa~ jueves Primetime App RRSS 5 20 Fema~
## 8 2021-09-30 34134 App_In~ jueves Resto App RRSS 5 4 Male
## 9 2021-09-30 33419 Web_yo~ jueves Mañana Web Yout~ 5 10 Fema~
## 10 2021-09-30 21119 Web_yo~ jueves Mañana Web Yout~ 5 9 Male
## # ... with 23,890 more rows, and 3 more variables: AgeGroup <fct>,
## # AdultsWithKids <fct>, RCH <fct>
# O también podemos pedir el corte mínimo
#datosMedia %>% slice_min(date)
# Summary para ver un poco la distribución de las variables
summary(datosMedia$date)## Min. 1st Qu. Median Mean 3rd Qu. Max.
## "2021-09-01" "2021-09-08" "2021-09-16" "2021-09-15" "2021-09-23" "2021-09-30"
Finalmente, habíamos supuesto bien y el dataset está ordenado por fecha, desde el 1 hasta el 30 de septiembre. Otra conclusiones son: 1) que hay en torno a 24 mil registros en los días 1 y 30. 2) que la distribución de la fecha es bastante uniforme, media y mediana en torno a mitad de mes, el tercer cuartil el día 23…pues parece que tenemos similar cantidad de registros por día.
Distintas posibilidades de programación en R
Nos planteamos ahora cuántos registros por hora tenemos. Para ello tenemos que contar, y esto es un paso muy importante a la hora de resumir y entender datos. En el formato de las clásicas queries de sql para consulta de bases de datos, tenemos un sinfín de posibilidades con R base, dplyr o data.table. La sintaxis, finalmente, es a gusto del consumidor pero hay ciertas generalidades que podemos tener en cuenta a la hora de elegir nuestra aproximación:
R base: es el tipo de programación primigenio de R, generalmente no se producen cambios que nos obliguen a modificar nuestro código como sucede con algunos paquetes. La sintaxis suele ser un poco menos legible y, en ocasiones puede resultar confusa por la estructura “anidada” de las consultas.
dplyr: muy buena opción para códigos estructurados y legibles a simple vista. en cuanto a tiempos, se comporta generalmente bien aunque las consultas u operaciones complejas con un volumen relativamente alto de datos pueden tardar bastante…mi preferido en cuanto a sintaxis.
data.table: opción favorita para amantes de la velocidad de proceso! Famoso por su rapidez en el manejo de grandes volúmenes de datos. La sintaxis es más parecida a a R base con estructura tipo datos[filas,columnas] pero en data.table se añade un tercer argumento generalmente reservado para agrupaciones con by=, esto da mucha flexibilidad para la programación.
Cualquier aproximación de código que encontremos por ahí es válida mientras cumpla el objetivo que nos marcamos. Generalmente mi opción es usar lo que me resulta más fácil en cada momento pero con cierta componente tendente a la optimización de tiempos… Así, algo que me resulta muy útil en el día a día (como asiduo usuario de dplyr) es la utilización del maravilloso paquete dtplyr que es capaz de traducir código en dplyr a código en data.table mediante la función show_query(). Muy recomendable cuando dplyr se queda lento.
# Utilizo dtplyr para traducir el slice_min que tanto tardaba.
lazy_dt(datosMedia) %>% slice_min(date) %>% show_query()## `_DT1`[, .SD[order(date)][frankv(date, ties.method = "min", na.last = "keep") <=
## 1L]]
# Aprovecho la query que me devuelve
data.table(datosMedia)[, .SD[order(date)][frankv(date, ties.method = "min",
na.last = "keep") <= 1L]]## date IdPanelist Medio DiaSem DayParting Plat Grupo
## 1: 2021-09-01 41240 TV_Antena3 miércoles Mañana TV A3Media
## 2: 2021-09-01 42501 App_Facebook miércoles Tarde App RRSS
## 3: 2021-09-01 73126 App_Instagram miércoles Mediodia App RRSS
## 4: 2021-09-01 56200 App_Instagram miércoles Mediodia App RRSS
## 5: 2021-09-01 22850 Web_youtube miércoles Tarde Web Youtube
## ---
## 24560: 2021-09-01 21007 TV_Antena3 miércoles Primetime TV A3Media
## 24561: 2021-09-01 65354 Web_tiktok miércoles Mañana Web RRSS
## 24562: 2021-09-01 2288 Web_instagram miércoles Mediodia Web RRSS
## 24563: 2021-09-01 32774 Web_twitter miércoles Tarde Web RRSS
## 24564: 2021-09-01 89904 Web_twitter miércoles Mañana Web RRSS
## Week hora Sex AgeGroup AdultsWithKids RCH
## 1: 1 9 Female 25 - 34 FALSE TRUE
## 2: 1 18 Male 45 + TRUE TRUE
## 3: 1 16 Female 18 - 24 TRUE TRUE
## 4: 1 14 Female 18 - 24 FALSE FALSE
## 5: 1 19 Female 25 - 34 FALSE FALSE
## ---
## 24560: 1 21 Male 45 + TRUE TRUE
## 24561: 1 12 Male 18 - 24 TRUE TRUE
## 24562: 1 16 Female 45 + TRUE TRUE
## 24563: 1 16 Female 18 - 24 FALSE FALSE
## 24564: 1 11 Female 18 - 24 FALSE FALSE
# Podemos comparar los tiempos con un microbenchmark
# microbenchmark::microbenchmark(
#
# dplyr = datosMedia %>% slice_min(date),
# data.table = data.table(datosMedia)[, .SD[order(date)][frankv(date, ties.method = "min",
# na.last = "keep") <= 1L]],
# times = 3
# )La diferencia es abismal… no es normal el comportamiento tan lento de slice_min. Para estas cosas nos viene genial la librería dtplyr, muy recomendable!
Nota: Recordad comentar (marcar y crtl+Mayus+c) la parte del benchmark a la hora de ejecutar el RMD porque tarda un rato!!
Contar, contar y contar
Como habíamos mencionado, saber contar bien lo que necesitamos es fundamental y nos ahorrará más de un apuro. Vamos a ir haciendo posible preguntas sobre el comportamiento de la gente e intentaremos darles respuesta con consultas sobre el dataset.
- ¿Hay duplicados? Cuantos registros distintos tenemos.
En este caso los duplicados serían mismo panelista, en el mismo medio, a la misma hora del mismo día puede aparecer varias veces. Vamos a comprobar.
## # A tibble: 301,360 x 13
## date IdPanelist Medio DiaSem DayParting Plat Grupo Week hora Sex
## <date> <dbl> <chr> <ord> <fct> <chr> <chr> <int> <dbl> <fct>
## 1 2021-09-01 41240 TV_Ant~ miérc~ Mañana TV A3Me~ 1 9 Fema~
## 2 2021-09-01 42501 App_Fa~ miérc~ Tarde App RRSS 1 18 Male
## 3 2021-09-01 73126 App_In~ miérc~ Mediodia App RRSS 1 16 Fema~
## 4 2021-09-01 56200 App_In~ miérc~ Mediodia App RRSS 1 14 Fema~
## 5 2021-09-01 22850 Web_yo~ miérc~ Tarde Web Yout~ 1 19 Fema~
## 6 2021-09-01 66366 App_In~ miérc~ Mediodia App RRSS 1 15 Male
## 7 2021-09-01 86041 App_Tw~ miérc~ Tarde App RRSS 1 17 Male
## 8 2021-09-01 7112 TV_Ant~ miérc~ Madrugada TV A3Me~ 1 1 Male
## 9 2021-09-01 79769 App_Fa~ miérc~ Mañana App RRSS 1 13 Fema~
## 10 2021-09-01 85549 App_Yo~ miérc~ Mañana App Yout~ 1 13 Fema~
## # ... with 301,350 more rows, and 3 more variables: AgeGroup <fct>,
## # AdultsWithKids <fct>, RCH <fct>
Tenemos 301360 registros únicos panelista-medio-día-hora, con lo que el resto (mas de la mitad) son duplicados. La pregunta siguiente es, esto es normal o tiene sentido? Y en principio no parece descabellado que alguien pueda entrar en facebook más de una vez por hora, no?
Esto nos da pie a generarnos un nuevo conjunto de datos que bien podría valer para predicción. Vamos a desarrollar la idea.
- ¿Cuantas visitas por hora hace cada panelista a cada medio?
Crearemos la variable visitasHora para recoger esta información. El dataset resultante tendrá 301360 filas y 14 columnas con lo que resumimos la información de los duplicados en una nueva variable, aligerando mucho el archivo y sin perder información. Ordenamos por valores descendentes de la nueva variable para hacernos una idea del máximo de visitas por hora de la gente.
# Generamos dataset sin duplicados con variable que recoja el número de repeticiones
datosMedia %>% group_by_all() %>% summarise(visitasHora=n()) %>% arrange(desc(visitasHora)) %>% ungroup()->datosMedia_distinct## `summarise()` has grouped output by 'date', 'IdPanelist', 'Medio', 'DiaSem', 'DayParting', 'Plat', 'Grupo', 'Week', 'hora', 'Sex', 'AgeGroup', 'AdultsWithKids'. You can override using the `.groups` argument.
Pues un récord de 564 visitas a la web de facebook el típico domingo por la tarde… Esta estructura de datos nos permitiría modelizar el número de visitas a la hora en función de perfil sexo-edad, variables temporales y medios plataforma y grupos.
- Que perfil tienen los grandes consumidores de medios?
Pediremos que nos muestre el summary de
# Perfil mediante tabla cruzada de panelistas con más de 100 visitas a la hora (independiente del medio que visiten)
datosMedia_distinct %>% filter(visitasHora > 100) %>%
select(IdPanelist,Sex,AgeGroup) %>% distinct() %>%
tabyl(Sex,AgeGroup) %>% adorn_totals(c("row", "col")) %>%
adorn_percentages("col") %>%
adorn_pct_formatting(rounding = "half up", digits = 0) %>%
adorn_ns() %>%
adorn_title("combined") %>%
knitr::kable()| Sex/AgeGroup | < 18 | 18 - 24 | 25 - 34 | 35 - 44 | 45 + | Total |
|---|---|---|---|---|---|---|
| Female |
|
75% (3) | 71% (5) | 33% (2) | 45% (5) | 54% (15) |
| Male |
|
25% (1) | 29% (2) | 67% (4) | 55% (6) | 46% (13) |
| N.A. |
|
0% (0) | 0% (0) | 0% (0) | 0% (0) | 0% (0) |
| Total |
|
100% (4) | 100% (7) | 100% (6) | 100% (11) | 100% (28) |
## [1] 1524
# Perfil mediante tabla cruzada de panelistas en la base general
datosMedia_distinct %>% select(IdPanelist,Sex,AgeGroup) %>% distinct() %>%
tabyl(Sex,AgeGroup) %>% adorn_totals(c("row", "col")) %>%
adorn_percentages("col") %>%
adorn_pct_formatting(rounding = "half up", digits = 0) %>%
adorn_ns() %>%
adorn_title("combined") %>%
knitr::kable()| Sex/AgeGroup | < 18 | 18 - 24 | 25 - 34 | 35 - 44 | 45 + | NA_ | Total |
|---|---|---|---|---|---|---|---|
| Female | 58% (7) | 70% (188) | 73% (296) | 70% (296) | 50% (199) | 75% (6) | 65% (992) |
| Male | 42% (5) | 30% (82) | 27% (111) | 30% (129) | 50% (202) | 0% (0) | 35% (529) |
| N.A. | 0% (0) | 0% (0) | 0% (1) | 0% (0) | 0% (0) | 25% (2) | 0% (3) |
| Total | 100% (12) | 100% (270) | 100% (408) | 100% (425) | 100% (401) | 100% (8) | 100% (1524) |
En la base general tenemos un 65% de mujeres y un 35% de hombres mientras que en el subconjunto de usuarios con más de 100 visitas a un medio concreto a la misma hora de un mismo día, tenemos una mayor balanceo 54-46, respectivamente. Esto nos indica que, en términos relativos, hay mayor porcentaje de hombres entre los que más visitas a la hora hacen.
- ¿Cuáles son los medios con más engagement a nivel horario?
Contamos la frecuencia de aparición delos distintos medios en la muestra de altas repetición.
## # A tibble: 5 x 2
## # Groups: Medio [5]
## Medio n
## <chr> <int>
## 1 App_Instagram 1
## 2 Web_facebook 45
## 3 Web_instagram 19
## 4 Web_twitter 4
## 5 Web_youtube 3
Webs de Facebook e Instagram como clara ganadoras.
- ¿Cuales son los medios con mayor número de visitas los sábados en prime time?
datosMedia_distinct %>% filter(DiaSem == 'sábado', DayParting =='Primetime') %>%
group_by(Medio) %>% summarise(vSabadoPrime=sum(visitasHora)) %>% arrange(desc(vSabadoPrime))## # A tibble: 49 x 2
## Medio vSabadoPrime
## <chr> <int>
## 1 App_Instagram 4926
## 2 Web_youtube 2810
## 3 App_Facebook 2219
## 4 Web_twitter 1589
## 5 Web_instagram 1401
## 6 Web_facebook 1210
## 7 App_Twitter 749
## 8 App_YouTube 742
## 9 App_TikTok 597
## 10 TV_Telecinco 492
## # ... with 39 more rows
- ¿Y considerando solo los medios de TV?
datosMedia_distinct %>% filter(DiaSem == 'sábado', DayParting =='Primetime', Plat=='TV') %>%
group_by(Medio) %>% summarise(vSabadoPrime=sum(visitasHora)) %>% arrange(desc(vSabadoPrime))## # A tibble: 13 x 2
## Medio vSabadoPrime
## <chr> <int>
## 1 TV_Telecinco 492
## 2 TV_Antena3 407
## 3 TV_Cuatro 258
## 4 TV_LaSexta 232
## 5 TV_FDF 172
## 6 TV_Neox 101
## 7 TV_Energy 89
## 8 TV_Mega 67
## 9 TV_Nova 59
## 10 TV_Boing 54
## 11 TV_Divinity 47
## 12 TV_Bmad 20
## 13 TV_Atreseries 3
Con estas cosas de código y estructura mental podemos preguntarnos y, lo más importante, sabemos respondernos casi cualquier cuestión sobre la información contenida en el dataset. Resulta muy importante tener el conocimiento de los datos a nivel descriptivo para poder llegar a buenos modelos predictivos.
Mino estudio de la variable creada como potencial respuesta a predecir
Distribución muy asimétrica. Ya veremos como tratar estas cosas. De momento, podemos pintar lo que esté por debajo de cierto umbral para poder ver algo de la distribución general.
Está claro que el grueso de la distribución se concentra en valore menores que 5. Esto podría cambiar la perspectiva del modelado hacia una aproximación por clasificación o regresión ordinal…
Continuará…
La idea es que vayamos completando este documento (cada uno lo customiza a su gusto) y nos queda como una chuleta de códigos potencialmente interesantes para la exploración de los datos.